import { promises as fs } from 'node:fs'
import { StdioServerTransport } from '@modelcontextprotocol/sdk/server/stdio.js'
import { Command } from 'commander'
import deepmerge from 'deepmerge'
import fsExtra from 'fs-extra'
import { detectPackageManager } from 'nypm'
import path from 'pathe'
import prompts from 'prompts'
import { x } from 'tinyexec'
import z from 'zod'
import { server } from '@/src/mcp'
import { loadEnvFiles } from '@/src/utils/env-loader'
import { getConfig } from '@/src/utils/get-config'
import { handleError } from '@/src/utils/handle-error'
import { highlighter } from '@/src/utils/highlighter'
import { logger } from '@/src/utils/logger'
import { spinner } from '@/src/utils/spinner'
import { updateDependencies } from '@/src/utils/updaters/update-dependencies'

const SHADCN_MCP_VERSION = 'latest'

const CLIENTS = [
  {
    name: 'claude',
    label: 'Claude Code',
    configPath: '.mcp.json',
    config: {
      mcpServers: {
        shadcnVue: {
          command: 'npx',
          args: [`shadcn-vue@${SHADCN_MCP_VERSION}`, 'mcp'],
        },
      },
    },
  },
  {
    name: 'cursor',
    label: 'Cursor',
    configPath: '.cursor/mcp.json',
    config: {
      mcpServers: {
        shadcnVue: {
          command: 'npx',
          args: [`shadcn-vue@${SHADCN_MCP_VERSION}`, 'mcp'],
        },
      },
    },
  },
  {
    name: 'vscode',
    label: 'VS Code',
    configPath: '.vscode/mcp.json',
    config: {
      servers: {
        shadcnVue: {
          command: 'npx',
          args: [`shadcn-vue@${SHADCN_MCP_VERSION}`, 'mcp'],
        },
      },
    },
  },
  {
    name: 'codex',
    label: 'Codex',
    configPath: '.codex/config.toml',
    config: `[mcp_servers.shadcn_vue]
command = "npx"
args = ["shadcn-vue@${SHADCN_MCP_VERSION}", "mcp"]
`,
  },
  {
    name: 'opencode',
    label: 'Opencode',
    configPath: 'opencode.json',
    config: {
      $schema: 'https://opencode.ai/config.json',
      mcp: {
        shadcnVue: {
          type: 'local',
          enabled: true,
          command: ['npx', `shadcn-vue@${SHADCN_MCP_VERSION}`, 'mcp'],
        },
      },
    },
  },
] as const

const DEPENDENCIES = [`shadcn-vue@${SHADCN_MCP_VERSION}`]

export const mcp = new Command()
  .name('mcp')
  .description('MCP server and configuration commands')
  .option(
    '-c, --cwd <cwd>',
    'the working directory. defaults to the current directory.',
    process.cwd(),
  )
  .action(async (options) => {
    try {
      await loadEnvFiles(options.cwd)
      const transport = new StdioServerTransport()
      await server.connect(transport)
    }
    catch (error) {
      logger.break()
      handleError(error)
    }
  })

const mcpInitOptionsSchema = z.object({
  client: z.enum(['claude', 'cursor', 'vscode', 'codex', 'opencode']),
  cwd: z.string(),
})

mcp
  .command('init')
  .description('Initialize MCP configuration for your client')
  .option(
    '--client <client>',
    `MCP client (${CLIENTS.map(c => c.name).join(', ')})`,
  )
  .action(async (opts, command) => {
    try {
      // Get the cwd from parent command.
      const parentOpts = command.parent?.opts() || {}
      const cwd = parentOpts.cwd || process.cwd()

      let client = opts.client

      if (!client) {
        const response = await prompts({
          type: 'select',
          name: 'client',
          message: 'Which MCP client are you using?',
          choices: CLIENTS.map(c => ({
            title: c.label,
            value: c.name,
          })),
        })

        if (!response.client) {
          logger.break()
          process.exit(1)
        }

        client = response.client
      }

      const options = mcpInitOptionsSchema.parse({
        client,
        cwd,
      })

      const config = await getConfig(options.cwd)

      if (options.client === 'codex') {
        if (config) {
          await updateDependencies([], DEPENDENCIES, config, {
            silent: false,
          })
        }
        else {
          const packageManager = await detectPackageManager(options.cwd)
          const installCommand = packageManager?.name === 'npm' ? 'install' : 'add'
          const devFlag = packageManager?.name === 'npm' ? '--save-dev' : '-D'

          const installSpinner = spinner('Installing dependencies...').start()
          await x(
            packageManager?.name || 'npm',
            [installCommand, devFlag, ...DEPENDENCIES],
            {
              nodeOptions: {
                cwd: options.cwd,
              },
            },
          )
          installSpinner.succeed('Installing dependencies.')
        }

        logger.break()
        logger.log('To configure the shadcn-vue MCP server in Codex:')
        logger.break()
        logger.log(
          `1. Open or create the file ${highlighter.info(
            '~/.codex/config.toml',
          )}`,
        )
        logger.log('2. Add the following configuration:')
        logger.log()
        logger.info(`[mcp_servers.shadcn_vue]
command = "npx"
args = ["shadcn-vue@${SHADCN_MCP_VERSION}", "mcp"]`)
        logger.break()
        logger.info('3. Restart Codex to load the MCP server')
        logger.break()
        process.exit(0)
      }

      const configSpinner = spinner('Configuring MCP server...').start()
      const configPath = await runMcpInit(options)
      configSpinner.succeed('Configuring MCP server.')

      if (config) {
        await updateDependencies([], DEPENDENCIES, config, {
          silent: false,
        })
      }
      else {
        const packageManager = await detectPackageManager(options.cwd)
        const installCommand = packageManager?.name === 'npm' ? 'install' : 'add'
        const devFlag = packageManager?.name === 'npm' ? '--save-dev' : '-D'

        const installSpinner = spinner('Installing dependencies...').start()
        await x(
          packageManager?.name || 'npm',
          [installCommand, devFlag, ...DEPENDENCIES],
          {
            nodeOptions: {
              cwd: options.cwd,
            },
          },
        )
        installSpinner.succeed('Installing dependencies.')
      }

      logger.break()
      logger.success(`Configuration saved to ${configPath}.`)
      logger.break()
    }
    catch (error) {
      handleError(error)
    }
  })

const overwriteMerge = (_: any[], sourceArray: any[]) => sourceArray

async function runMcpInit(options: z.infer<typeof mcpInitOptionsSchema>) {
  const { client, cwd } = options

  const clientInfo = CLIENTS.find(c => c.name === client)
  if (!clientInfo) {
    throw new Error(
      `Unknown client: ${client}. Available clients: ${CLIENTS.map(
        c => c.name,
      ).join(', ')}`,
    )
  }

  const configPath = path.join(cwd, clientInfo.configPath)
  const dir = path.dirname(configPath)
  await fsExtra.ensureDir(dir)

  let existingConfig = {}
  try {
    const content = await fs.readFile(configPath, 'utf-8')
    existingConfig = JSON.parse(content)
  }
  catch {}

  const mergedConfig = deepmerge(
    existingConfig,
    clientInfo.config as Record<string, unknown>,
    { arrayMerge: overwriteMerge },
  )

  await fs.writeFile(
    configPath,
    `${JSON.stringify(mergedConfig, null, 2)}\n`,
    'utf-8',
  )

  return clientInfo.configPath
}
